#  Wrapper.py -- Meeting point between Proctor.py and Trac.py
#  Copyright (C) 2008 Zachary Voase <cracka80@gmail.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
#  02110-1301, USA.

"""ProctorTicket.Wrapper
The library which holds together ProctorTicket.Trac and ProctorTicket.Proctor,
enabling a result to be turned into a ticket and added to the database. This
module contains cmdParser, an option parser instance for the command line
interface. It also contains a function, makeSense, for grabbing all the info from
the parsed arguments and returning a TracDBConnection instance, along with a
dictionary of settings."""

import Proctor
import Trac
import Utils
import optparse
import os
import time

global cmdParser

cmdParser = optparse.OptionParser()
cmdParser.add_option( 
    "-u", "--update", 
    dest="update", 
    action="store_true", 
    default=False, 
    help="""If tickets are found with the same summary but different
descriptions, update them.""" )
cmdParser.add_option( 
    "--unique", 
    dest="unique_field", 
    metavar="UNIQUE_FIELD_NAME", 
    default="summary", 
    help="""Specify which field should be considered unique when determining
whether a ticket already exists, default is 'summary'.""" )
cmdParser.add_option( 
    "-e", "--env-path", 
    dest="env_path", 
    metavar="/path/to/env/", 
    default=None, 
    help="""Path to Trac environment. Without the --keyword argument,
environment should be appended to the end of the command, after all options have
been specified. With the --keyword option, this option must be given, or an
error will be produced. If you only use one Trac installation, you can also
set the 'TRAC_ENV' environment variable to point to the Trac folder.""" )
cmdParser.add_option( 
    "-k", "--keywords", 
    dest="keywords", 
    action="store_true", 
    default=False, 
    help="""Specify that keywords will be added to tickets. If set, keywords
should be appended to the end of the command, after all options have been
specified. With this option, the '--env-path' option is required, or an error will
be produced. See the help for '--env-path' for more information.""" )
cmdParser.add_option( 
    "-t", "--type", 
    dest="type", 
    default="defect", 
    help="""Type of tickets, i.e. 'defect', 'enhancement', 'etc', default is 'defect'.""" )
cmdParser.add_option( 
    "--no-type", 
    dest="no_type", 
    action="store_true", 
    default=False, 
    help="""Do not set a type for the tickets.""" )
cmdParser.add_option( 
    "-p", "--priority", 
    dest="priority", 
    default="normal", 
    help="""Priority of tickets, i.e. 'high', 'normal', 'low', default is 'normal'""" )
cmdParser.add_option( 
    "--no-priority", 
    dest="no_priority", 
    action="store_true", 
    default=False, 
    help="""Do not set a priority for the tickets.""" )
cmdParser.add_option( 
    "-c", "--component", 
    dest="component", 
    default="", 
    help="""Component to which tickets belong.""" )
cmdParser.add_option( 
    "-s", "--severity", 
    dest="severity", 
    default="normal", 
    help="""Severity of tickets, i.e. 'major', 'normal', 'minor', default is 'normal'.
Alternatively, the --no-severity option may be specified, in which case this
option will be overridden and the severity will not be set.""" )
cmdParser.add_option( 
    "--no-severity", 
    dest="no_severity", 
    action="store_true", 
    default=False, 
    help="""Turns off severity field (see --severity). Set this if you don't use
'severity' in your installation, which is the default for some recent versions
of Trac.""" )
cmdParser.add_option( 
    "-o", "--owner", 
    dest="owner", 
    default="", 
    help="""Assigned owner of tickets.""" )
cmdParser.add_option( 
    "-r", "--reporter", 
    dest="reporter", 
    default="", 
    help="""Reporter of tickets.""" )
cmdParser.add_option( 
    "--carbon-copy", 
    metavar="someaddress@somedomain.org", 
    dest="carbon_copy", 
    default="", 
    help="""Carbon copy recipient email address for tickets.""" )
cmdParser.add_option( 
    "-v", "--affected-version", 
    dest="affected_version", 
    default="", 
    help="""Affected version of tickets.""" )
cmdParser.add_option( 
    "--status", 
    dest="status", 
    default="new", 
    help="""Set status of tickets, default is 'new'.""" )
cmdParser.add_option( 
    "--no-status", 
    dest="no_status", 
    action="store_true", 
    default=False, 
    help="""Suppress addition of a status field to ticket.""" )
cmdParser.add_option( 
    "-m", "--milestone", 
    dest="milestone", 
    default="", 
    help="""Set milestone for resolution of tickets.""" )
cmdParser.add_option( 
    "--time", 
    dest="time", 
    type="int", 
    help="""Specify a different time in seconds from the Epoch, default is
current time as returned by system clock.""" )
cmdParser.add_option( 
    "--no-time", 
    dest="no_time", 
    action="store_true", 
    default=False, 
    help="""Causes the database to generate the time field itself rather than it
being specified in the query. This means that tickets from the same run may have
different times, due to lags in querying the database.""" )
cmdParser.add_option( 
    "--change-time", 
    dest="change_time", 
    type="int", 
    default=0, 
    help="""Specify a last-updated time. This is usually only used for updated
tickets, so it is not recommended.""" )
cmdParser.add_option( 
    "-l", "--license", 
    dest="license", 
    action="store_true", 
    default=False, 
    help="""Show the license for this program.""" )
cmdParser.add_option( 
    "--log-file", 
    metavar="/path/to/logfile.log", 
    dest="logfile", 
    help="""Specify to use the output of a previously run proctorbatch test instead
of running one. The output file must have been generated using the '--oarsable'
option by proctorbatch, otherwise errors will occur.""" )
cmdParser.add_option( 
    "-d", "--test-directory", 
    dest="testdir", 
    metavar="/path/to/testdir", 
    help="""Specify a root directory from which to run proctorbatch. By default,
proctorbatch will run in the current directory, and recurse through all sub-directories
found. By changing the directory from which this happens, you can narrow down the
tests which are run.""" )

def cmpOptions( opt1, opt2 ):
    """cmpOptions(<optparse.Option>, <optparse.Option>):
Comparison function for options which just compares them by their
long option names."""
    return cmp( opt1._long_opts[0].lstrip( "-" ), opt2._long_opts[0].lstrip( "-" ) )

cmdParser.option_list = sorted( cmdParser.option_list, cmp=cmpOptions )
cmdParser.version = "0.1"
cmdParser.usage = """    %prog --keywords --env-path=/path/to/env [ options ] keyword1 keyword2 ...
    %prog [ options ] /path/to/env
    %prog --help
    %prog --version"""
cmdParser.description = """A Python program to automatically turn Proctor unit tests into Trac tickets,
by parsing out errors, generating data on each one and placing it into a
Trac installation, if a similar ticket has not already been created.

%prog is free software, and you are welcome to redistribute it
under certain conditions; type '%prog --license' for details."""

def ProcessOptions( arguments=None ):
    """ProcessOptions(arguments=None):
Takes the fruit of optparse's efforts and produces two dictionaries,
the first with ticket-specific settings and the other with general
Proctor and environment settings. In addition, arguments can be
specified by setting the 'arguments' keyword argument. If this is not
set, then by default sys.argv will be used."""
    if arguments is not None: options, args = cmdParser.parse_args( arguments )
    else: options, args = cmdParser.parse_args()
    ticketDict = {}
    optionDict = {}
    if options.license:
        print """ProctorTicket:: Convert Proctor unit tests into Trac tickets.
Copyright (C) 2008 Zachary Voase <cracka80@gmail.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA."""
        return
    if options.logfile is None:
        optionDict["runtest"] = True
        if options.testdir is None:
            optionDict["testdir"] = os.getcwdu()
        else:
            optionDict["testdir"] = unicode( options.testdir )
    else:
        optionDict["runtest"] = False
        optionDict["logfilename"] = unicode( options.logfile )
    if options.unique_field in Trac._TICKET_FIELDS:
        optionDict["unique_field"] = options.unique_field
    else:
        optionDict["unique_field"] = 'summary'
    optionDict["update"] = options.update
    if options.keywords is False:
        if len( args ) >= 1:
            optionDict["envpath"] = args[0]
        elif options.env_path is not None:
            optionDict["envpath"] = options.env_path
        elif os.environ.has_key( "TRAC_ENV" ):
            optionDict["envpath"] = os.environ["TRAC_ENV"]
        else:
            raise Exception( "Error: Please specify a Trac environment path!" )
        ticketDict["keywords"] = []
    elif options.keywords is True:
        if options.env_path is None and not os.environ.has_key( "TRAC_ENV" ):
            raise Exception( "Error: Please specify a Trac environment path!" )
        elif options.env_path is None and os.environ.has_key( "TRAC_ENV" ):
            optionDict["envpath"] = os.environ["TRAC_ENV"]
        else: optionDict["envpath"] = options.env_path
        ticketDict["keywords"] = list( map( str.strip, map( str.lower, args ) ) )
    if options.no_severity is True: ticketDict["severity"] = ""
    else: ticketDict["severity"] = options.severity
    if options.no_status is True: ticketDict["status"] = ""
    else: ticketDict["status"] = options.status
    if options.no_time is True: ticketDict["time"] = 0
    else: ticketDict["time"] = int( time.strftime( "%s" ) )
    if options.no_priority is True: ticketDict["priority"] = ""
    else: ticketDict["priority"] = options.priority
    if options.no_type is True: ticketDict["type"] = ""
    else: ticketDict["type"] = options.type
    ticketDict["component"] = options.component
    ticketDict["owner"] = options.owner
    ticketDict["reporter"] = options.reporter
    ticketDict["cc"] = options.carbon_copy
    ticketDict["version"] = options.affected_version
    ticketDict["milestone"] = options.milestone
    ticketDict["changetime"] = options.change_time
    ticketDict = Utils.dictFilter( Utils.dictEmptiness, ticketDict )
    return ( ticketDict, optionDict )

def MakeClasses( ticketDict, optionDict ):
    """MakeClasses(<ticketDict: dictionary>, <optionDict: dictionary>):
Creates a Trac.TracDBConn and a Proctor.Test object given a ticketDict and an
optionDict, as output from the ProcessOptions function. Populates the Test
Object with test data, depending on whether there is a log file or a new test
needs to be run."""
    DBConn = Trac.TracDBConn( optionDict["envpath"], preLoadDict=ticketDict, update=optionDict["update"] )
    TestObject = Proctor.Test()
    if optionDict["runtest"]:
        TestObject.loadFromTest( directory=optionDict["testdir"] )
    else:
        logfd = open( optionDict["logfilename"] )
        TestObject.loadFromLog( logfd )
        logfd.close()
    return ( DBConn, TestObject )

def AddTickets( DBConn, TestObject, field="summary" ):
    """AddTickets(<DBConn: Trac.TracDBConn>, <TestObject: Proctor.Test>, <fieldname: string("summary")>):
Adds the results from TestObject as tickets to DBConn, completing the process
of migrating Proctor unit tests to Trac tickets. Accepts the keyword argument
'field', which specifies what the unique field should be that is used to look
up whether or not a particular ticket already exists."""
    ticketDicts = [ Utils.Results2TicketDict( resultsList ) for resultsList in TestObject.resultsDict.values() ]
    pre_existing = 0
    new = 0
    errors = 0
    update = 0
    for ticketDict in ticketDicts:
        try:
            id, type = DBConn.checkAndAddOrUpdate( ticketDict, field=field, preLoad=True )
            if type == "update":
                update += 1
            else:
                new += 1
        except AssertionError:
            pre_existing += 1
#        except:
#            errors += 1
    print """%d new tickets added
%d tickets updated
%d pre-existing tickets
%d errors occured""" % ( new, update, pre_existing, errors )
    return

def main():
    ticketDict, optionDict = ProcessOptions()
    DBConn, TestObject = MakeClasses( ticketDict, optionDict )
    AddTickets( DBConn, TestObject, field=optionDict["unique_field"] )
    return

if __name__ == "__main__":
    main()
